Introduzione

Il task POS è molto importante per il NLP, in quanto permette di identificare la categoria grammaticale di ciascuna parola in un testo. Nel web sono presenti numerosi esempi applicativi del POS tagging per la lingua inglese, ma pochi o nessuno per altre lingue. Si presenta qui un modesto tentativo di addestrare un POS tagger per la lingua italiana, basato su una semplice rete neurale con word embedding. L’obiettivo di questo esercizio è quello di fornire un esempio di come addestrare un POS tagger per la lingua italiana, utilizzando un dataset di esempio, ma anche di trovare una soluzione accettabile che richieda un ridotto costo computazionale, sia nel training sia nella produzione di previsioni.

Dataset

Il dataset utilizzato proviene dal github repository DLCL 204: Digital Humanities Across Borders condotto dall’Università di Stanford. Il datataset è in formato conllu, che è un formato standard per rappresentare alberi di dipendenza. Il dataset è stato convertito in un dataframe di R, e successivamente è stato trasformato in un corpus di testo con la libreria quanteda.I tags sono stati attribuiti manualmente a ciascun token attraverso il software TINT, con la relativa notazione. Questo dataset si presta ad altri task oltre al POS tagging, come il dependency parsing, il named entity recognition, e altri ancora.

TINT si basa sul ISTD (Italian Stanford Dependency Treebank), e utilizza i seguenti Part-of-Speech tag.

Value Description Examples Contexts of use
A adjective bello, buono, pauroso, ottimo una bella passeggiata un ottimo attaccante una persona paurosa
AP possessive adjective mio, tuo, nostro, loro mio parere il tuo libro
B adverb bene, fortemente, malissimo, domani arrivo domani sto bene
BN negative adverb no, non, nemmeno non arrivo nemmeno domani non sto bene
CC coordinate conjunction e, o, ma i libri e i quaderni
CS subordinate conjunction mentre, quando quando ho finito vengo
DD demonstrative determiner questo, codesto, quello questo denaro quella famiglia
DE exclamative determiner che, quale, quanto che disastro! quale catastrofe!
DI indefinite determiner alcuno, certo, tale, parecchio, qualsiasi alcune telefonate parecchi giornali qualsiasi persona
DR relative determiner cui, quale cui libri
DQ interrogative determiner che, quale, quanto che cosa quanta strada quale formazione
E preposition di, a, da, in, su, attraverso, verso, di a casa del poeta prima di giorno verso sera
EA articulated preposition del, alla, dal a casa del poeta all’ alba
FB balanced punctuation ( ) - frutta (pere e banane)
FC clause boundary punctuation , ; mele, pere e banane.
FF comma , - mele, pere e banane
FS sentence boundary punctuation ; . ? ! cosa vuoi?
I interjection ahimè, beh, ecco, grazie Beh, che vuoi?
N cardinal number uno, due, cento, mille, 28, 2000 due partite 28 anni
NO ordinal number primo, secondo, centesimo secondo posto
PC clitic pronoun ci, vi, ne vi piace?
PD demonstrative pronoun questo, quello, costui quello di Roma costui uccide
PE personal pronoun stressed: io, tu, egli, lui, noi, voi, essi unstressed: lo, la, mi, ci, vi io parto lo mangio
PI indefinite pronoun chiunque, ognuno, molto chiunque venga i diritti di ognuno
PP possessive pronoun mio, tuo, suo, loro, proprio il mioè qui più bella della loro
PR relative pronoun che, cui, quale ciò che dice il quale afferma a cui parlo
PQ interrogative pronoun che, chi, quanto non so chi parta quanto costa? che ha fatto ieri?
RD determinative article il, lo, la, i, gli, le il libro i gatti
RI indeterminative article uno, un, una un amico una bambina
S common noun amico, insegnante, verità l’amico la verità
SP proper noun Monica, Pisa, Fiat, Sardegna Monica scrive
SW foreign noun fazenda, mulieris dignitatem, weekend una fazenda in Brasile il prossimo weekend
T predeterminer tutti i giorni tutti i giorni entrambi i genitori
V verb mangio, avere, passato il tempo passa le scriverò una lettera vengo domani
VA auxiliary verb sono stato, ho mangiato il peggio è passato ho scritto una lettera
VM modal verb posso, devono il peggio deve venire dovrò scriverle una lettera
X residual class: it includes formulae, unclassified words, alphabetic symbols and the like distanziare di piacce

librerie utilizzate

library(dplyr)    #per efficienza computazionale e pulizia del codice
## Warning: il pacchetto 'dplyr' è stato creato con R versione 4.4.2
## 
## Caricamento pacchetto: 'dplyr'
## I seguenti oggetti sono mascherati da 'package:stats':
## 
##     filter, lag
## I seguenti oggetti sono mascherati da 'package:base':
## 
##     intersect, setdiff, setequal, union
library(udpipe)   #per la lettura del file conllu
## Warning: il pacchetto 'udpipe' è stato creato con R versione 4.4.2
library(quanteda) #per il preprocessing del testo
## Package version: 4.0.2
## Unicode version: 15.1
## ICU version: 74.1
## Parallel computing: 8 of 8 threads used.
## See https://quanteda.io for tutorials and examples.
library(keras)    #per il deep learning
## Warning: il pacchetto 'keras' è stato creato con R versione 4.4.2

importazione e lettura dataset

Nell’importazione dei dati dobbiamo convertire il file connlu in un dataframe, con una variabile per i token e una per i tags.

path = 'https://raw.githubusercontent.com/quinnanya/dlcl204/refs/heads/master/italian/it_isdt-ud-train.conllu'
dataset = udpipe_read_conllu(path)
dim(dataset)
## [1] 294397     14
colnames(dataset)
##  [1] "doc_id"        "paragraph_id"  "sentence_id"   "sentence"     
##  [5] "token_id"      "token"         "lemma"         "upos"         
##  [9] "xpos"          "feats"         "head_token_id" "dep_rel"      
## [13] "deps"          "misc"
str(dataset)
## 'data.frame':    294397 obs. of  14 variables:
##  $ doc_id       : chr  "# newdoc = tanl" "# newdoc = tanl" "# newdoc = tanl" "# newdoc = tanl" ...
##  $ paragraph_id : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ sentence_id  : chr  "isst_tanl-1" "isst_tanl-1" "isst_tanl-2" "isst_tanl-2" ...
##  $ sentence     : chr  "LONDRA." "LONDRA." "Gas dalla statua." "Gas dalla statua." ...
##  $ token_id     : chr  "1" "2" "1" "2-3" ...
##  $ token        : chr  "LONDRA" "." "Gas" "dalla" ...
##  $ lemma        : chr  "Londra" "." "gas" NA ...
##  $ upos         : chr  "PROPN" "PUNCT" "NOUN" NA ...
##  $ xpos         : chr  "SP" "FS" "S" NA ...
##  $ feats        : chr  NA NA "Gender=Masc" NA ...
##  $ head_token_id: chr  "0" "1" "0" NA ...
##  $ dep_rel      : chr  "root" "punct" "root" NA ...
##  $ deps         : chr  "0:root" "1:punct" "0:root" NA ...
##  $ misc         : chr  "SpaceAfter=No" NA NA NA ...

La nostra variabile target sarà xpos, che contiene i tag POS.

tab = table(dataset$xpos)
tab
## 
##     A    AP     B    BN    CC    CS    DD    DE    DI    DQ    DR     E    FB 
## 17349  1701  8701  1827  7549  2833  1014     4  1502   875    39 41893  5436 
##    FC    FF    FS     I     N    NO  PART    PC    PD    PE    PI    PP    PQ 
##  1241 12692 11925    62  4795   976    24  4333   770   647   949    29   823 
##    PR    RD    RI     S    SP    Sw    SW   SYM     T     V    VA    VM     X 
##  2879 35237  4284 54975 13670     1   213    88   379 26560  5923  1758    63

Vista la bassa frequenza relativa di alcuni tag, è verosimile che il modello non riesca a prevederli correttamente (ad esempio X, Sw, PP,.. ). Questa debolezza del dataset difficilmente potrà essere rimossa attraverso il training. Per rigore, sarebbe opportuno eliminare questi tag dal dataset, perciò li sostituiamo con NA.

freqs = as.numeric(tab)
id_poco_frequenti = which(freqs < quantile(freqs, 0.2))
nomi_rari = names(tab)[id_poco_frequenti]
dataset1 = dataset   #copia di backup
dataset$xpos[dataset$xpos %in% nomi_rari] = NA
tab = table(dataset$xpos)
tab
## 
##     A    AP     B    BN    CC    CS    DD    DI    DQ     E    FB    FC    FF 
## 17349  1701  8701  1827  7549  2833  1014  1502   875 41893  5436  1241 12692 
##    FS     N    NO    PC    PD    PE    PI    PQ    PR    RD    RI     S    SP 
## 11925  4795   976  4333   770   647   949   823  2879 35237  4284 54975 13670 
##    SW     T     V    VA    VM 
##   213   379 26560  5923  1758

In questo modo abbiamo ridotto il numero di valori da prevedere da 39 a 31.

Pre-processing

  1. costruire un id univoco per identificare le singole frasi, in modo da poterle ricomporre in seguito
  2. costruire un dizionario di token rilevanti (utilizzati almeno 10 volte in tutto il corpus, per economicità)
  3. associare ad ogni token un valore intero, in modo da poter utilizzare l’emebdding layer di keras (ogni intero verrà associato al vettore onehot con 1 nella posizione corrispondente)
  4. associare un indice a ciascun tag, per poter utilizzare la funzione to_categorical di keras e trasformare i tag in un vettore onehot. Dobbiamo tener traccia dell’ordinamento di questi indici per poter svolgere future previsioni.

Funzioni utili per il pre-processing

Ogni parola/token non sarà una stringa ma un intero corripspondente al suo indice nel dizionario. Questo indice sarà utilizzato per creare un vettore onehot per l’embedding layer di keras.

## converto ogni parola nel suo indice nel dizionario
find_tok_id = function(x, dizionario){
    if (x %in% dizionario){
        id = which(x==dizionario)
    }else{
        id = 0
    }
    return(id)
}

Per il training sarà necessario procurare una matrice di contesto per ogni parola. In questa matrice, ogni osservazione è una sequenza di parole, dalle k precedenti alle k successive rispetto alla parola target. Questa matrice sarà utilizzata per addestrare il modello, in modo che possa prevedere il tag di una parola in base al contesto in cui si trova. Il numero di parole nel dataset è 294397, ed è quindi richiesto un approccio efficiente per la costruzione di questa matrice.

##estraggo gli id di ogni frase
##poi dentro ogni frase estraggo ogni parola/token
##per ogni parola, considerata dentro il sottovettore della frase, 
##calcolo il vettore contesto di un'osservazione solo se il suo tag non è NA


context_matrix = function(tokeID, sentID, filtroNA, k){

    C = 2*k+1
    N = length(tokeID)
    matrice = matrix(NA, nrow=N, ncol=C)

    for (i in 1:N){
        if (filtroNA[i]){
            idsS = which(sentID==sentID[i])
            idsT = (i-k):(i+k)
            filtro2 = (idsT %in% idsS) & (idsT > 0) & (idsT < N)
            idsF = idsT[filtro2]
            tid = rep(0, C)
            tid[filtro2] = tokeID[idsF]
            matrice[i,] = tid
        }
    }
    matrice = matrice[filtroNA,]
    return(matrice)
}

Estrazione del dizionario

Il dizionario verrà costruito attraverso una matrice-documento termine sulle trasformazioni lowercase di tutti i token trovati. Questo dizionario sarà utilizzato per convertire le parole in interi. Se una parola non comparirà al suo interno, il suo indice sarà 0. Il dizionario esclude le parole a bassa frequenza. Ponendo un limite complessivo di 10 apparizioni, si riduce il numero di parole da 26893 a 2800, producendo un rilevante risparmio computazionale.

dtm = dataset$token %>% 
tolower() %>% 
tokens()  %>% 
dfm()

dim(dtm)
## [1] 294397  26893
dtm = dtm %>% 
dfm_trim(min_termfreq = 10)

dim(dtm)
## [1] 294397   2800
dizionario = colnames(dtm)

inserimento di nuove variabili

  • toke: tutte le parole lowercase
  • tokeID: l’indice di ciascun token nel dizionario
  • sentID: id univoco per ciascuna frase
start = Sys.time()
## aggrego tutti i dati e creo l'id per frase
data1 = dataset %>% mutate(
    sentID = paste0(doc_id, paragraph_id, sentence_id),
    toke = token %>% tolower() ) %>%
mutate(
tokeID = sapply(toke, function(x){find_tok_id(x, dizionario)})
)
dim(data1)
## [1] 294397     17
Sys.time() - start
## Time difference of 16.05207 secs

Costruzione delle matrici di input e di output

Questa è la sezione del codice più onerosa in termini computazionali, seconda solo al training di grandi modelli. La matrice di contesto va costruita per ogni parola, e dobbiamo trasformare i tag in una matrice onehot. Si è visto che con le sole prime 50000 osservazioni il tempo di esecuzione è di circa 20 secondi. Usandole tutte e 294000, cresce di molto.

dim(data1)
## [1] 294397     17
data2 = data1[1:100000,] 
dim(data2)
## [1] 100000     17

pre-precessing output

start = Sys.time()
filtro = !is.na(data2$xpos)
tags = data2$xpos[filtro]
tags_unici = unique(tags)
y_ids = sapply(tags, function(x){find_tok_id(x,tags_unici)})
y_ids = unlist(as.matrix(y_ids))
tagsmat = to_categorical(y_ids)
Sys.time() - start
## Time difference of 10.5633 secs
dim(tagsmat)
## [1] 94075    32
head(tagsmat)
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
## [1,]    0    1    0    0    0    0    0    0    0     0     0     0     0     0
## [2,]    0    0    1    0    0    0    0    0    0     0     0     0     0     0
## [3,]    0    0    0    1    0    0    0    0    0     0     0     0     0     0
## [4,]    0    0    0    0    1    0    0    0    0     0     0     0     0     0
## [5,]    0    0    0    0    0    1    0    0    0     0     0     0     0     0
## [6,]    0    0    0    1    0    0    0    0    0     0     0     0     0     0
##      [,15] [,16] [,17] [,18] [,19] [,20] [,21] [,22] [,23] [,24] [,25] [,26]
## [1,]     0     0     0     0     0     0     0     0     0     0     0     0
## [2,]     0     0     0     0     0     0     0     0     0     0     0     0
## [3,]     0     0     0     0     0     0     0     0     0     0     0     0
## [4,]     0     0     0     0     0     0     0     0     0     0     0     0
## [5,]     0     0     0     0     0     0     0     0     0     0     0     0
## [6,]     0     0     0     0     0     0     0     0     0     0     0     0
##      [,27] [,28] [,29] [,30] [,31] [,32]
## [1,]     0     0     0     0     0     0
## [2,]     0     0     0     0     0     0
## [3,]     0     0     0     0     0     0
## [4,]     0     0     0     0     0     0
## [5,]     0     0     0     0     0     0
## [6,]     0     0     0     0     0     0

Bisogna notare che to_categorical restituisce un numero di colonne pari al numero delle classi + 1. Pertanto in sede di previsione bisogna aver chiari i riferimenti di ogni colonna ad ogni classe. è utile salvare l’associazione di ogni indice di output alla sua stringa.

y_dict = unique(y_ids)
t(y_dict)
##      SP FS S E RD A FC RI VA VM  V PR FB PI FF CC  N PC AP NO  B  T PE CS DI BN
## [1,]  1  2 3 4  5 6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
##      PD DD DQ PQ SW
## [1,] 27 28 29 30 31

pre-processing input

Nella costruzione di ogni vettore di contesto, si utilizzano i 5 token precedenti e i 5 token successivi al token di input cui va assegnato un pos-tag.

start = Sys.time()
wordmat = context_matrix(tokeID = data2$tokeID, 
                         sentID = data2$sentID,
                        filtroNA = !is.na(data2$xpos), 
                        k=5)
Sys.time()-start
## Time difference of 1.015322 mins
dim(wordmat)
## [1] 94075    11
head(wordmat)
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11]
## [1,]    0    0    0    0    0    1    2    0    0     0     0
## [2,]    0    0    0    0    1    2    0    0    0     0     0
## [3,]    0    0    0    0    0    3    4    5    6     0     2
## [4,]    0    0    0    3    4    5    6    0    2     0     0
## [5,]    0    0    3    4    5    6    0    2    0     0     0
## [6,]    0    3    4    5    6    0    2    0    0     0     0

final data for train and test

input_matrix = wordmat #%>% as.matrix()
output_matrix = tagsmat #%>% as.matrix()

num_samples <- nrow(input_matrix)       # Numero di campioni
timesteps <- ncol(input_matrix)         # Numero di timesteps
num_classes <- ncol(output_matrix)      # Numero di classi (colonne di output_matrix)
num_samples
## [1] 94075
timesteps
## [1] 11
num_classes
## [1] 32

Il dataset è stato diviso in training e test set, con una proporzione di 80% e 20% rispettivamente. Nel corso del training si utilizzerà un 20% del training set come validation set. Per cui si hanno complessivamente: - 20% al test set - 16% al validation set - 64% al training set

N = nrow(input_matrix)
train_id = sample(1:N, round(N*0.8))

train_input = input_matrix[train_id,]
train_output = output_matrix[train_id,]
test_input = input_matrix[-train_id,]
test_output = output_matrix[-train_id,]

training del modello

In teoria un layer ricorrente sarebbe più adatto per questo task, ma per motivi di efficienza computazionale si è scelto un modello più semplice, con un layer di embedding, un layer fully connected e un layer softmax per la classificazione. Si sono provati diversi modelli, con diversi layer e diversi parametri, per valutare quale fosse il migliore.

# Definisci i parametri principali
embedding_dim <- 50  # Dimensione del word embedding
lstm_units <- 50     # Numero di unità nel layer LSTM
pr_dropout <- 0.9       # Probabilità di dropout

definizione modelli

model_1 <- keras_model_sequential() %>%
  # Layer di embedding (indice parole -> vettore embedding)
  layer_embedding(input_dim = length(dizionario) + 1, # Dimensione vocabolario (+1 per l'indice 0)
                  output_dim = embedding_dim,
                  input_length = timesteps) %>%
  layer_flatten() %>%
  # Layer fully connected
  layer_dense(units = 100, activation = "relu") %>%
  # Layer softmax per la classificazione
  layer_dense(units = num_classes, activation = "softmax")%>% 
compile(
  optimizer = "adam",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)
model_1
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  embedding (Embedding)              (None, 11, 50)                  140050      
##  flatten (Flatten)                  (None, 550)                     0           
##  dense_1 (Dense)                    (None, 100)                     55100       
##  dense (Dense)                      (None, 32)                      3232        
## ================================================================================
## Total params: 198382 (774.93 KB)
## Trainable params: 198382 (774.93 KB)
## Non-trainable params: 0 (0.00 Byte)
## ________________________________________________________________________________
model_2 <- keras_model_sequential() %>%
  layer_embedding(input_dim = length(dizionario) + 1,
                  output_dim = embedding_dim,
                  input_length = timesteps) %>%
  layer_flatten() %>%
  layer_dense(units = 100, activation = "relu") %>%
  # riduco l'overfitting con il dropout
  layer_dropout(pr_dropout) %>%
  layer_dense(units = num_classes, activation = "softmax")%>% 
compile(
  optimizer = "adam",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)
model_2
## Model: "sequential_1"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  embedding_1 (Embedding)            (None, 11, 50)                  140050      
##  flatten_1 (Flatten)                (None, 550)                     0           
##  dense_3 (Dense)                    (None, 100)                     55100       
##  dropout (Dropout)                  (None, 100)                     0           
##  dense_2 (Dense)                    (None, 32)                      3232        
## ================================================================================
## Total params: 198382 (774.93 KB)
## Trainable params: 198382 (774.93 KB)
## Non-trainable params: 0 (0.00 Byte)
## ________________________________________________________________________________
model_3 <- keras_model_sequential() %>%
  layer_embedding(input_dim = length(dizionario) + 1,
                  output_dim = embedding_dim,
                  input_length = timesteps) %>%
  layer_flatten() %>%
  layer_dense(units = 100, activation = "relu") %>%
  layer_dropout(pr_dropout) %>%
  layer_dense(units = 50, activation = "relu") %>%
  layer_dropout(pr_dropout) %>%
  layer_dense(units = num_classes, activation = "softmax")%>%
compile(
  optimizer = "adam",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)  
model_3
## Model: "sequential_2"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  embedding_2 (Embedding)            (None, 11, 50)                  140050      
##  flatten_2 (Flatten)                (None, 550)                     0           
##  dense_6 (Dense)                    (None, 100)                     55100       
##  dropout_2 (Dropout)                (None, 100)                     0           
##  dense_5 (Dense)                    (None, 50)                      5050        
##  dropout_1 (Dropout)                (None, 50)                      0           
##  dense_4 (Dense)                    (None, 32)                      1632        
## ================================================================================
## Total params: 201832 (788.41 KB)
## Trainable params: 201832 (788.41 KB)
## Non-trainable params: 0 (0.00 Byte)
## ________________________________________________________________________________

Si presenta anche un modello con 2 layer LSTM (uno per percorrere la sequenza in avanti e uno per percorrerla all’indietro). Intuiyivamente, questo modello dovrebbe essere più adatto per questo task, ma richiede un costo computazionale maggiore. Inoltre i risultati non lo hanno dimostrato.

model_4 <- keras_model_sequential() %>%
  layer_embedding(input_dim = length(dizionario) + 1,
                  output_dim = embedding_dim,
                  input_length = timesteps) %>%
  # Layer LSTM bidirectional 
  bidirectional(layer_lstm(units = lstm_units, 
                           activation='relu')) %>%
  layer_dense(units = 50, activation = "relu") %>%
  layer_dropout(pr_dropout) %>%
  layer_dense(units = num_classes, activation = "softmax")%>% 
compile(
  optimizer = "adam",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)
model_4
## Model: "sequential_3"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  embedding_3 (Embedding)            (None, 11, 50)                  140050      
##  bidirectional (Bidirectional)      (None, 100)                     40400       
##  dense_8 (Dense)                    (None, 50)                      5050        
##  dropout_3 (Dropout)                (None, 50)                      0           
##  dense_7 (Dense)                    (None, 32)                      1632        
## ================================================================================
## Total params: 187132 (730.98 KB)
## Trainable params: 187132 (730.98 KB)
## Non-trainable params: 0 (0.00 Byte)
## ________________________________________________________________________________

training modelli

ep = 20
start = Sys.time()
history1 <- model_1 %>% fit(
  x = train_input,
  y = train_output,
  batch_size = 100,  # Dimensione del batch
  epochs = ep,      # Numero di epoche
  validation_split = 0.2  # Percentuale di dati di validazione
)
## Epoch 1/20
## 603/603 - 5s - loss: 1.1549 - accuracy: 0.6857 - val_loss: 0.5135 - val_accuracy: 0.8439 - 5s/epoch - 9ms/step
## Epoch 2/20
## 603/603 - 3s - loss: 0.3993 - accuracy: 0.8799 - val_loss: 0.3649 - val_accuracy: 0.8881 - 3s/epoch - 6ms/step
## Epoch 3/20
## 603/603 - 3s - loss: 0.2914 - accuracy: 0.9088 - val_loss: 0.3365 - val_accuracy: 0.8922 - 3s/epoch - 6ms/step
## Epoch 4/20
## 603/603 - 3s - loss: 0.2374 - accuracy: 0.9252 - val_loss: 0.3325 - val_accuracy: 0.8932 - 3s/epoch - 5ms/step
## Epoch 5/20
## 603/603 - 3s - loss: 0.1941 - accuracy: 0.9378 - val_loss: 0.3387 - val_accuracy: 0.8930 - 3s/epoch - 5ms/step
## Epoch 6/20
## 603/603 - 3s - loss: 0.1557 - accuracy: 0.9512 - val_loss: 0.3543 - val_accuracy: 0.8912 - 3s/epoch - 5ms/step
## Epoch 7/20
## 603/603 - 3s - loss: 0.1239 - accuracy: 0.9628 - val_loss: 0.3757 - val_accuracy: 0.8885 - 3s/epoch - 6ms/step
## Epoch 8/20
## 603/603 - 3s - loss: 0.0979 - accuracy: 0.9718 - val_loss: 0.4007 - val_accuracy: 0.8861 - 3s/epoch - 5ms/step
## Epoch 9/20
## 603/603 - 3s - loss: 0.0818 - accuracy: 0.9765 - val_loss: 0.4288 - val_accuracy: 0.8862 - 3s/epoch - 6ms/step
## Epoch 10/20
## 603/603 - 3s - loss: 0.0662 - accuracy: 0.9815 - val_loss: 0.4540 - val_accuracy: 0.8823 - 3s/epoch - 6ms/step
## Epoch 11/20
## 603/603 - 3s - loss: 0.0555 - accuracy: 0.9849 - val_loss: 0.4859 - val_accuracy: 0.8817 - 3s/epoch - 6ms/step
## Epoch 12/20
## 603/603 - 3s - loss: 0.0482 - accuracy: 0.9861 - val_loss: 0.5167 - val_accuracy: 0.8782 - 3s/epoch - 6ms/step
## Epoch 13/20
## 603/603 - 4s - loss: 0.0410 - accuracy: 0.9880 - val_loss: 0.5589 - val_accuracy: 0.8769 - 4s/epoch - 6ms/step
## Epoch 14/20
## 603/603 - 4s - loss: 0.0368 - accuracy: 0.9893 - val_loss: 0.5969 - val_accuracy: 0.8710 - 4s/epoch - 7ms/step
## Epoch 15/20
## 603/603 - 3s - loss: 0.0337 - accuracy: 0.9902 - val_loss: 0.6279 - val_accuracy: 0.8738 - 3s/epoch - 6ms/step
## Epoch 16/20
## 603/603 - 3s - loss: 0.0300 - accuracy: 0.9906 - val_loss: 0.6421 - val_accuracy: 0.8736 - 3s/epoch - 6ms/step
## Epoch 17/20
## 603/603 - 3s - loss: 0.0269 - accuracy: 0.9917 - val_loss: 0.6782 - val_accuracy: 0.8728 - 3s/epoch - 6ms/step
## Epoch 18/20
## 603/603 - 4s - loss: 0.0249 - accuracy: 0.9925 - val_loss: 0.7155 - val_accuracy: 0.8653 - 4s/epoch - 6ms/step
## Epoch 19/20
## 603/603 - 3s - loss: 0.0225 - accuracy: 0.9932 - val_loss: 0.7292 - val_accuracy: 0.8701 - 3s/epoch - 6ms/step
## Epoch 20/20
## 603/603 - 4s - loss: 0.0210 - accuracy: 0.9933 - val_loss: 0.7548 - val_accuracy: 0.8689 - 4s/epoch - 6ms/step
'tempo trascorso'
## [1] "tempo trascorso"
Sys.time() - start
## Time difference of 1.183856 mins
'tempo trascorso per epoca'
## [1] "tempo trascorso per epoca"
(Sys.time() - start)/ep
## Time difference of 0.05919602 mins

Questo modello presenta un eveidente problema di overfitting.

plot(history1)

ep = 20
start = Sys.time()
history2 <- model_2 %>% fit(
  x = train_input,
  y = train_output,
  batch_size = 100,  # Dimensione del batch
  epochs = ep,      # Numero di epoche
  validation_split = 0.2  # Percentuale di dati di validazione
)
## Epoch 1/20
## 603/603 - 6s - loss: 2.0918 - accuracy: 0.4088 - val_loss: 1.1430 - val_accuracy: 0.6517 - 6s/epoch - 9ms/step
## Epoch 2/20
## 603/603 - 4s - loss: 1.3243 - accuracy: 0.5776 - val_loss: 0.8059 - val_accuracy: 0.7651 - 4s/epoch - 6ms/step
## Epoch 3/20
## 603/603 - 4s - loss: 1.1217 - accuracy: 0.6255 - val_loss: 0.6678 - val_accuracy: 0.7921 - 4s/epoch - 6ms/step
## Epoch 4/20
## 603/603 - 4s - loss: 1.0297 - accuracy: 0.6476 - val_loss: 0.6007 - val_accuracy: 0.8095 - 4s/epoch - 6ms/step
## Epoch 5/20
## 603/603 - 4s - loss: 0.9744 - accuracy: 0.6642 - val_loss: 0.5531 - val_accuracy: 0.8217 - 4s/epoch - 6ms/step
## Epoch 6/20
## 603/603 - 4s - loss: 0.9469 - accuracy: 0.6715 - val_loss: 0.5279 - val_accuracy: 0.8320 - 4s/epoch - 6ms/step
## Epoch 7/20
## 603/603 - 4s - loss: 0.9119 - accuracy: 0.6820 - val_loss: 0.5085 - val_accuracy: 0.8396 - 4s/epoch - 6ms/step
## Epoch 8/20
## 603/603 - 4s - loss: 0.8957 - accuracy: 0.6841 - val_loss: 0.4949 - val_accuracy: 0.8413 - 4s/epoch - 7ms/step
## Epoch 9/20
## 603/603 - 4s - loss: 0.8771 - accuracy: 0.6894 - val_loss: 0.4837 - val_accuracy: 0.8490 - 4s/epoch - 7ms/step
## Epoch 10/20
## 603/603 - 4s - loss: 0.8611 - accuracy: 0.6928 - val_loss: 0.4793 - val_accuracy: 0.8518 - 4s/epoch - 6ms/step
## Epoch 11/20
## 603/603 - 4s - loss: 0.8472 - accuracy: 0.6973 - val_loss: 0.4758 - val_accuracy: 0.8538 - 4s/epoch - 6ms/step
## Epoch 12/20
## 603/603 - 4s - loss: 0.8364 - accuracy: 0.7012 - val_loss: 0.4687 - val_accuracy: 0.8576 - 4s/epoch - 6ms/step
## Epoch 13/20
## 603/603 - 4s - loss: 0.8213 - accuracy: 0.7053 - val_loss: 0.4628 - val_accuracy: 0.8629 - 4s/epoch - 6ms/step
## Epoch 14/20
## 603/603 - 4s - loss: 0.8129 - accuracy: 0.7080 - val_loss: 0.4650 - val_accuracy: 0.8610 - 4s/epoch - 6ms/step
## Epoch 15/20
## 603/603 - 4s - loss: 0.8041 - accuracy: 0.7132 - val_loss: 0.4645 - val_accuracy: 0.8643 - 4s/epoch - 6ms/step
## Epoch 16/20
## 603/603 - 4s - loss: 0.7869 - accuracy: 0.7159 - val_loss: 0.4646 - val_accuracy: 0.8644 - 4s/epoch - 7ms/step
## Epoch 17/20
## 603/603 - 4s - loss: 0.7808 - accuracy: 0.7181 - val_loss: 0.4624 - val_accuracy: 0.8670 - 4s/epoch - 6ms/step
## Epoch 18/20
## 603/603 - 4s - loss: 0.7716 - accuracy: 0.7196 - val_loss: 0.4641 - val_accuracy: 0.8666 - 4s/epoch - 6ms/step
## Epoch 19/20
## 603/603 - 4s - loss: 0.7637 - accuracy: 0.7235 - val_loss: 0.4703 - val_accuracy: 0.8682 - 4s/epoch - 6ms/step
## Epoch 20/20
## 603/603 - 4s - loss: 0.7564 - accuracy: 0.7275 - val_loss: 0.4650 - val_accuracy: 0.8693 - 4s/epoch - 6ms/step
'tempo trascorso'
## [1] "tempo trascorso"
Sys.time() - start
## Time difference of 1.302896 mins
'tempo trascorso per epoca'
## [1] "tempo trascorso per epoca"
(Sys.time() - start)/ep
## Time difference of 0.06514668 mins
plot(history2)

ep = 20
start = Sys.time()
history3 <- model_3 %>% fit(
  x = train_input,
  y = train_output,
  batch_size = 100,  # Dimensione del batch
  epochs = ep,      # Numero di epoche
  validation_split = 0.2  # Percentuale di dati di validazione
)
## Epoch 1/20
## 603/603 - 6s - loss: 2.8640 - accuracy: 0.1778 - val_loss: 2.1326 - val_accuracy: 0.4240 - 6s/epoch - 9ms/step
## Epoch 2/20
## 603/603 - 4s - loss: 2.2522 - accuracy: 0.3206 - val_loss: 1.7842 - val_accuracy: 0.4740 - 4s/epoch - 7ms/step
## Epoch 3/20
## 603/603 - 4s - loss: 2.0559 - accuracy: 0.3590 - val_loss: 1.6392 - val_accuracy: 0.4878 - 4s/epoch - 7ms/step
## Epoch 4/20
## 603/603 - 4s - loss: 1.9633 - accuracy: 0.3990 - val_loss: 1.5391 - val_accuracy: 0.5396 - 4s/epoch - 7ms/step
## Epoch 5/20
## 603/603 - 4s - loss: 1.8951 - accuracy: 0.4213 - val_loss: 1.4825 - val_accuracy: 0.5650 - 4s/epoch - 7ms/step
## Epoch 6/20
## 603/603 - 4s - loss: 1.8516 - accuracy: 0.4285 - val_loss: 1.4291 - val_accuracy: 0.5688 - 4s/epoch - 7ms/step
## Epoch 7/20
## 603/603 - 4s - loss: 1.7961 - accuracy: 0.4338 - val_loss: 1.3788 - val_accuracy: 0.5690 - 4s/epoch - 7ms/step
## Epoch 8/20
## 603/603 - 4s - loss: 1.7822 - accuracy: 0.4340 - val_loss: 1.3459 - val_accuracy: 0.5612 - 4s/epoch - 7ms/step
## Epoch 9/20
## 603/603 - 4s - loss: 1.7625 - accuracy: 0.4375 - val_loss: 1.3412 - val_accuracy: 0.5870 - 4s/epoch - 7ms/step
## Epoch 10/20
## 603/603 - 4s - loss: 1.7428 - accuracy: 0.4461 - val_loss: 1.3226 - val_accuracy: 0.6036 - 4s/epoch - 7ms/step
## Epoch 11/20
## 603/603 - 4s - loss: 1.7170 - accuracy: 0.4538 - val_loss: 1.3097 - val_accuracy: 0.6080 - 4s/epoch - 7ms/step
## Epoch 12/20
## 603/603 - 4s - loss: 1.7126 - accuracy: 0.4553 - val_loss: 1.3033 - val_accuracy: 0.6093 - 4s/epoch - 7ms/step
## Epoch 13/20
## 603/603 - 4s - loss: 1.7016 - accuracy: 0.4575 - val_loss: 1.2999 - val_accuracy: 0.5960 - 4s/epoch - 7ms/step
## Epoch 14/20
## 603/603 - 4s - loss: 1.6798 - accuracy: 0.4601 - val_loss: 1.2947 - val_accuracy: 0.6196 - 4s/epoch - 7ms/step
## Epoch 15/20
## 603/603 - 5s - loss: 1.6828 - accuracy: 0.4646 - val_loss: 1.2978 - val_accuracy: 0.6383 - 5s/epoch - 8ms/step
## Epoch 16/20
## 603/603 - 4s - loss: 1.6625 - accuracy: 0.4683 - val_loss: 1.2865 - val_accuracy: 0.6424 - 4s/epoch - 7ms/step
## Epoch 17/20
## 603/603 - 4s - loss: 1.6629 - accuracy: 0.4680 - val_loss: 1.2946 - val_accuracy: 0.6218 - 4s/epoch - 7ms/step
## Epoch 18/20
## 603/603 - 4s - loss: 1.6557 - accuracy: 0.4711 - val_loss: 1.2873 - val_accuracy: 0.6102 - 4s/epoch - 7ms/step
## Epoch 19/20
## 603/603 - 4s - loss: 1.6416 - accuracy: 0.4741 - val_loss: 1.2801 - val_accuracy: 0.6333 - 4s/epoch - 7ms/step
## Epoch 20/20
## 603/603 - 4s - loss: 1.6313 - accuracy: 0.4786 - val_loss: 1.2826 - val_accuracy: 0.6408 - 4s/epoch - 7ms/step
'tempo trascorso'
## [1] "tempo trascorso"
Sys.time() - start
## Time difference of 1.422874 mins
'tempo trascorso per epoca'
## [1] "tempo trascorso per epoca"
(Sys.time() - start)/ep
## Time difference of 0.07114578 mins
plot(history3)

ep = 20
start = Sys.time()
history4 <- model_4 %>% fit(
  x = train_input,
  y = train_output,
  batch_size = 100,  # Dimensione del batch
  epochs = ep,      # Numero di epoche
  validation_split = 0.2  # Percentuale di dati di validazione
)
## Epoch 1/20
## 603/603 - 15s - loss: 2.9029 - accuracy: 0.1621 - val_loss: 2.4654 - val_accuracy: 0.2022 - 15s/epoch - 24ms/step
## Epoch 2/20
## 603/603 - 9s - loss: 2.4184 - accuracy: 0.2515 - val_loss: 1.8763 - val_accuracy: 0.4140 - 9s/epoch - 14ms/step
## Epoch 3/20
## 603/603 - 8s - loss: 2.0139 - accuracy: 0.3592 - val_loss: 1.5057 - val_accuracy: 0.5302 - 8s/epoch - 14ms/step
## Epoch 4/20
## 603/603 - 8s - loss: 1.8642 - accuracy: 0.4015 - val_loss: 1.4461 - val_accuracy: 0.5659 - 8s/epoch - 14ms/step
## Epoch 5/20
## 603/603 - 10s - loss: 1.7874 - accuracy: 0.4293 - val_loss: 1.3725 - val_accuracy: 0.5618 - 10s/epoch - 16ms/step
## Epoch 6/20
## 603/603 - 11s - loss: 1.7026 - accuracy: 0.4576 - val_loss: 1.2533 - val_accuracy: 0.6303 - 11s/epoch - 19ms/step
## Epoch 7/20
## 603/603 - 11s - loss: 1.6302 - accuracy: 0.4902 - val_loss: 1.1734 - val_accuracy: 0.6633 - 11s/epoch - 18ms/step
## Epoch 8/20
## 603/603 - 10s - loss: 1.5860 - accuracy: 0.5096 - val_loss: 1.1625 - val_accuracy: 0.6864 - 10s/epoch - 17ms/step
## Epoch 9/20
## 603/603 - 9s - loss: 1.5606 - accuracy: 0.5116 - val_loss: 1.0817 - val_accuracy: 0.7000 - 9s/epoch - 15ms/step
## Epoch 10/20
## 603/603 - 9s - loss: 1.5161 - accuracy: 0.5240 - val_loss: 1.1170 - val_accuracy: 0.6956 - 9s/epoch - 15ms/step
## Epoch 11/20
## 603/603 - 9s - loss: 1.4821 - accuracy: 0.5289 - val_loss: 1.1151 - val_accuracy: 0.6980 - 9s/epoch - 15ms/step
## Epoch 12/20
## 603/603 - 9s - loss: 1.4608 - accuracy: 0.5351 - val_loss: 0.9964 - val_accuracy: 0.7024 - 9s/epoch - 15ms/step
## Epoch 13/20
## 603/603 - 9s - loss: 1.4296 - accuracy: 0.5410 - val_loss: 1.0403 - val_accuracy: 0.7027 - 9s/epoch - 15ms/step
## Epoch 14/20
## 603/603 - 9s - loss: 1.3832 - accuracy: 0.5526 - val_loss: 0.9763 - val_accuracy: 0.7182 - 9s/epoch - 15ms/step
## Epoch 15/20
## 603/603 - 9s - loss: 1.4388 - accuracy: 0.5364 - val_loss: 0.9111 - val_accuracy: 0.7178 - 9s/epoch - 15ms/step
## Epoch 16/20
## 603/603 - 9s - loss: 1.3942 - accuracy: 0.5424 - val_loss: 0.9369 - val_accuracy: 0.7239 - 9s/epoch - 15ms/step
## Epoch 17/20
## 603/603 - 9s - loss: 1.3663 - accuracy: 0.5558 - val_loss: 1.1128 - val_accuracy: 0.7347 - 9s/epoch - 15ms/step
## Epoch 18/20
## 603/603 - 9s - loss: 1.3531 - accuracy: 0.5602 - val_loss: 1.0265 - val_accuracy: 0.7372 - 9s/epoch - 15ms/step
## Epoch 19/20
## 603/603 - 9s - loss: 1.3619 - accuracy: 0.5578 - val_loss: 1.0171 - val_accuracy: 0.7384 - 9s/epoch - 15ms/step
## Epoch 20/20
## 603/603 - 9s - loss: 1.3348 - accuracy: 0.5641 - val_loss: 1.1643 - val_accuracy: 0.7318 - 9s/epoch - 15ms/step
'tempo trascorso'
## [1] "tempo trascorso"
Sys.time() - start
## Time difference of 3.16823 mins
'tempo trascorso per epoca'
## [1] "tempo trascorso per epoca"
(Sys.time() - start)/ep
## Time difference of 0.1584136 mins
plot(history4)

testing

confronto tra i test

Il test set sembra premiare la semplicità. Utilizzeremo il modello 2 come modello di riferimento.

# Valutazione e confronto dei modelli
score1 <- model_1 %>% evaluate(test_input, test_output)
## 588/588 - 1s - loss: 0.7585 - accuracy: 0.8725 - 1s/epoch - 2ms/step
score2 <- model_2 %>% evaluate(test_input, test_output)
## 588/588 - 2s - loss: 0.4602 - accuracy: 0.8683 - 2s/epoch - 3ms/step
score3 <- model_3 %>% evaluate(test_input, test_output)
## 588/588 - 1s - loss: 1.2738 - accuracy: 0.6451 - 1s/epoch - 2ms/step
score4 <- model_4 %>% evaluate(test_input, test_output)
## 588/588 - 2s - loss: 0.9495 - accuracy: 0.7348 - 2s/epoch - 4ms/step
mat = rbind(score1,score2,score3, score4)
rownames(mat) = c('embedding di base', 'modello singolo dropout', 'modello doppio dropout', 
                  'modello lstm bidirezionale')
print(mat)
##                                 loss  accuracy
## embedding di base          0.7584625 0.8724954
## modello singolo dropout    0.4601650 0.8683497
## modello doppio dropout     1.2738203 0.6451236
## modello lstm bidirezionale 0.9494894 0.7347861

F1 score sulle classi

test_actual = apply(test_output, MARGIN=1, which.max)
test_actual = rownames(y_dict)[test_actual]
test_actual = factor(test_actual, levels=rownames(y_dict))


test_preds = model_2 %>% predict(test_input)
## 588/588 - 1s - 1s/epoch - 2ms/step
test_preds = apply(test_preds, MARGIN=1, which.max)
test_preds = rownames(y_dict)[test_preds]
test_preds = factor(test_preds, levels=rownames(y_dict))

tab = table(test_preds, test_actual)
sum(diag(tab))/sum(tab)
## [1] 0.8688577
#diag(tab)
#tab
recalls = diag(tab)/colSums(tab)
precisions = diag(tab)/rowSums(tab)
F1_scores = round((2*recalls*precisions)/(recalls+precisions), 3)
names(F1_scores) = rownames(y_dict)
F1_scores
##    SP    FS     S     E    RD     A    FC    RI    VA    VM     V    PR    FB 
##   NaN 0.488 0.991 0.854 0.984 0.954 0.646 1.000 0.890 0.799 0.867 0.810 0.795 
##    PI    FF    CC     N    PC    AP    NO     B     T    PE    CS    DI    BN 
## 0.975 0.390 0.971 0.987 0.790 0.880 0.925 0.089 0.877   NaN 0.675 0.733 0.667 
##    PD    DD    DQ    PQ    SW 
## 0.994 0.545 0.750   NaN   NaN

utilizzo pratico del pos tagger

Si vuole qui utilizzare il pos tagger per prevedere i tag di un nuovo testo.

associazione di ogni tag alla sua descrizione

livelli = rownames(y_dict)  ##livelli di output
## associazione più intuitiva di ciascun indice di output

chiavi = c('A','AP','B','BN','CC','CS','DD','DE','DI','DR','DQ','E',
           'EA','FB','FC','FF','FS','I','N','NO','PC','PD','PE','PI',
           'PP','PR','PQ','RD','RI','S','SP','SW','T','V','VA','VM','X')
descrizioni = c(
    'adjective', 'possessive adjective', 'adverb', 'negative adverb', 'coordinate conjunction', 'subordinate conjunction', 'demonstrative determiner', 'exclamative determiner', 'indefinite determiner', 'relative determiner', 'interrogative determiner', 'preposition', 'articulated preposition', 'balanced punctuation', 'clause boundary punctuation', 'comma', 'sentence boundary punctuation', 'interjection', 'cardinal number', 'ordinal number', 'clitic pronoun', 'demonstrative pronoun', 'personal pronoun', 'indefinite pronoun', 'possessive pronoun', 'relative pronoun', 'interrogative pronoun', 'determinative article', 'indeterminative article', 'common noun', 'proper noun', 'foreign noun', 'predeterminer', 'verb', 'auxiliary verb', 'modal verb', 'residual class')

dictio = list()
for (i in 1:length(chiavi)){
    dictio[chiavi[i]] = descrizioni[i]
}
dictio['A']
## $A
## [1] "adjective"

funzione per fare il pos tagging

L’accuracy è vicina al 90%. Si vuole qui anche applicare le previsioni del modello a nuove frasi.

pos_tagger = function(text, model){
  # Tokenize into sentences
  sentences <- tokens(text, what = "sentence")[[1]]
  
  # Create a data frame to store words and their sentence ID
  word_data <- data.frame()
  
  # Loop through sentences, tokenize words, and assign sentence ID
  for (i in seq_along(sentences)) {
    words <- tokens(sentences[i], what = "word")[[1]] 
    temp_df <- data.frame(word = words, sentence_id = i)
    word_data <- bind_rows(word_data, temp_df)
  }
  
  toks = word_data$word %>% tolower()
  sent_id = word_data$sentence_id
  
  words = sapply(toks,function(x){find_tok_id(x,dizionario)})
  input_mat = context_matrix(words, 
                             sentID=sent_id,  
                             filtroNA = rep(TRUE, length(words)), 
                             k = 5)
  preds = predict(model, input_mat, verbose=0)
  preds = apply(preds, MARGIN=1, which.max) 
  preds = preds - 1        #to_categorical prevede una colonna in più all'inizio 
  tags0 = livelli[preds]
  tags1 = unlist(dictio[tags0])
  names(tags1) = NULL
  names(tags0) = NULL
  
  mat = rbind(toks, tags0, tags1)
  rownames(mat) = c('token', 'tag', 'descrizione')
  
  return(mat)
}

Di seguito una dimostrazione di utilizzo. I risultati sembrano accettabili, anche se non perfetti.

frase = 'Io adoro dare da mangiare al mio cane mentre leggo il giornale. 
A volte però vuole mangiare anche la carta. Ora devo andare, se no faccio tardi.'

pos_tagger(frase, model_2)
##             [,1]               [,2]    [,3]   [,4]          [,5]      
## token       "io"               "adoro" "dare" "da"          "mangiare"
## tag         "PE"               "V"     "V"    "E"           "V"       
## descrizione "personal pronoun" "verb"  "verb" "preposition" "verb"    
##             [,6]          [,7]                   [,8]         
## token       "al"          "mio"                  "cane"       
## tag         "SP"          "AP"                   "S"          
## descrizione "proper noun" "possessive adjective" "common noun"
##             [,9]                      [,10]   [,11]                  
## token       "mentre"                  "leggo" "il"                   
## tag         "CS"                      "V"     "RD"                   
## descrizione "subordinate conjunction" "verb"  "determinative article"
##             [,12]         [,13]                           [,14]        
## token       "giornale"    "."                             "a"          
## tag         "S"           "FS"                            "E"          
## descrizione "common noun" "sentence boundary punctuation" "preposition"
##             [,15]         [,16]    [,17]        [,18]      [,19]   
## token       "volte"       "però"   "vuole"      "mangiare" "anche" 
## tag         "S"           "B"      "VM"         "V"        "B"     
## descrizione "common noun" "adverb" "modal verb" "verb"     "adverb"
##             [,20]                   [,21]        
## token       "la"                    "carta"      
## tag         "RD"                    "S"          
## descrizione "determinative article" "common noun"
##             [,22]                           [,23]    [,24]        [,25]   
## token       "."                             "ora"    "devo"       "andare"
## tag         "FS"                            "B"      "VM"         "V"     
## descrizione "sentence boundary punctuation" "adverb" "modal verb" "verb"  
##             [,26]   [,27]                     [,28]        [,29]    [,30]   
## token       ","     "se"                      "no"         "faccio" "tardi" 
## tag         "FF"    "CS"                      "VM"         "V"      "B"     
## descrizione "comma" "subordinate conjunction" "modal verb" "verb"   "adverb"
##             [,31] 
## token       "."   
## tag         "V"   
## descrizione "verb"
library(knitr)
## Warning: il pacchetto 'knitr' è stato creato con R versione 4.4.2
library(kableExtra)
## Warning: il pacchetto 'kableExtra' è stato creato con R versione 4.4.2
## 
## Caricamento pacchetto: 'kableExtra'
## Il seguente oggetto è mascherato da 'package:dplyr':
## 
##     group_rows
knitr::kable(t(pos_tagger(frase, model_2)))
token tag descrizione
io PE personal pronoun
adoro V verb
dare V verb
da E preposition
mangiare V verb
al SP proper noun
mio AP possessive adjective
cane S common noun
mentre CS subordinate conjunction
leggo V verb
il RD determinative article
giornale S common noun
. FS sentence boundary punctuation
a E preposition
volte S common noun
però B adverb
vuole VM modal verb
mangiare V verb
anche B adverb
la RD determinative article
carta S common noun
. FS sentence boundary punctuation
ora B adverb
devo VM modal verb
andare V verb
, FF comma
se CS subordinate conjunction
no VM modal verb
faccio V verb
tardi B adverb
. V verb

conclusione

I risultati non sono eccellenti, ma sono comunque accettabili per un modello così semplice, e rappresentano se mai una buona base di partenza per eventuali ulteriori sviluppi con modelli RNN, LSTM o GRU che possono richiedere un costo computazionale maggiore.